Pool Size 에 대해

@VERO
Created Date · 2023년 10월 03일 08:10
Last Updated Date · 2023년 10월 17일 15:10

Pool Size에 대해 를 번역하였습니다.

다른 변경 사항이 없는 상태에서 커넥션 풀 크기만 줄였을 때 애플리케이션 응답 시간이 최대 100ms 에서 2ms 로 50배 이상 개선되었다는 Oracle 의 성능 측정 결과가 있었다.

실제로는 단일 코어는 한 번의 하나의 스레드만 실행할 수 있고, 운영체제가 컨텍스트를 전환할 때 해당 코어가 다른 스레드의 코드를 실행하는 방식으로 실행된다.
물론 하나의 CPU 리소스가 A, B 를 순차적으로 실행하는 것이 타임 슬라이싱으로 A, B 를 동시에 실행하는 것보다 항상 더 빠르다. 스레드 수가 CPU 코어 수를 초과하면 더 많은 스레드를 추가하여 속도가 빨라지는 것이 아니라 느려진다.

데이터베이스의 주요 병목 현상

CPU, 디스크, 네트워크

간단하게 생각하면 컴퓨팅 코어가 8개인 서버에서 연결 수를 8개로 설정하면 최적의 성능을 제공할 수 있고, 그 이상이면 컨텍스트 전환의 오버헤드로 인해 속도가 느려지기 시작할 것이다.

그렇지만 디스크와 네트워크도 무시할 수는 없다.

데이터베이스는 일반적으로 읽기 / 쓰기 헤드가 있는 디스크에 데이터를 저장한다. 읽기 / 쓰기 헤드는 한 번에 한 곳에만 있을 수 있기 떄문에 다른 쿼리에 대한 데이터를 읽기 / 쓰기 하려면 새로운 위치로 탐색해야 한다. 즉, 탐색 시간 비용이 발생하고, 디스크가 플래터에서 데이터가 읽기 / 쓰기를 위해 다시 돌아올 때까지 기다려야 하는 회전 비용도 발생한다. (캐싱이 좀 더 효율적으로 도와주겠지만, 기본적으로는 이런 과정을 거쳐야 한다.)

I/O 대기시간 동안 커넥션 / 쿼리 / 스레드는 디스크를 기다리면서 단순하게 'block' 된다.
이 시간 동안 OS 는 다른 스레드에 대한 코드를 더 실행하여 해당 CPU 리소스를 더 잘 사용할 수 있다. 즉, 스레드가 I/O 에서 차단되는 시간이 존재하기 때문에, 실제로는 물리적인 컴퓨팅 코어 수보다 많은 수의 커넥션 / 스레드를 보유하여 더 많은 작업을 수행할 수 있는 것이다.

얼마나 더 많은지는 디스크 서브시스템에 따라 다르다. 최신 SSD 드라이브에는 탐색 시간 비용, 회전 요인이 없다.
'SSD가 더 빠르니 더 많은 스레드를 사용할 수 있다' 는 말이 아니다. 더 빠르고, 탐색이 없고, 회전 지연이 없다는 것은 block 이 적다는 것을 의미하기 때문에 더 적은 수의 스레드가 더 많은 스레드보다 더 나은 성능을 발휘할 수 있다. 더 많은 스레드는 blocking 으로 인해 실행 기회가 생길 때만 더 나은 성능을 발휘하는 것이다.

네트워크도 디스크와 유사하다. 이더넷 인터페이스를 통해 유선으로 데이터를 사용하는 경우에도 송수신 버퍼가 가득 차서 멈출 때 blocking 이 발생할 수 있다. 그러나 네트워크는 리소스 block 측면에서는 계산에서 생략해도 괜찮다.

postgresql-benchmark.png

위의 PostgreSQL 벤치마크에서 약 50개의 커넥션에서 TPS 속도가 평탄해진다. 16코어, 32코어를 사용하지 않는다면 커넥션 96개도 너무 많다.

공식

애플리케이션을 테스트하고, 이 공식을 중심으로 다양한 풀 설정을 시도해보아야 한다.

connections=(corecount2+effectiveSpindleCount)connections = (corecount* 2 + effectiveSpindleCount)

(코어수는 하이퍼 스레딩이 활성화 되어 있더라도 HT 스레드를 포함해서는 안 된다.)

effectiveSpindleCount 란?

스토리지의 회전하는 디스크(HDD) 수를 의미한다.
기본적으로 스토리지가 데이터를 빠르게 액세스 하려면 여러 HDD 를 동시에 사용하여 다중 I/O 작업을 수행할 수 있어야 한다. 하지만 데이터가 메모리에 완전히 캐시되어 있으면 디스크 액세스가 필요하지 않으므로 effectiveSpindleCount 는 0이 된다. 캐시 히트율이 떨어질수록, 스토리지 액세스의 필요성이 높아지므로 effectiveSpindleCount 는 실제 스핀들의 숫자에 접근하게 된다.

Ex. 4코어 I7 서버에 HDD 하나가 있다면 커넥션 풀의 크기는 다음과 같아야 한다.

9=(42+1)9 = (4 * 2 + 1)

값이 작아보이지만, 해당 설정에서 3000명의 프론트엔드 사용자가 6000TPS 로 간단한 쿼리를 실행하는 것을 쉽게 처리할 수 있다.

프론트엔드 사용자가 10000명일 때 10000개의 커넥션 풀을 갖는 것은 미친 짓이다. 1000개도 끔찍하다. 100개도 과하다. 최대 수십 개의 연결로 구성된 소규모 풀이 필요하며, 나머지 애플리케이션 스레드는 풀에서 커넥션을 기다리는 상태로 block 되어야 한다.
CPU 코어 * 2 를 초과하는 경우는 거의 없다.

조금 더 알아봤는데... CPU 바운드가 높은 작업에서만 해당되는 공식일 수도

Pool Locking

단일 액터가 많은 커넥션을 얻는 것과 관련해서 Pool Locking 의 가능성이 제기되었다. 그러나 대부분 응용 프로그램 수준의 문제이다. 물론 풀 크기를 늘리면 이런 시나리오에서 lock 을 완화할 수 있지만, 풀을 확장하기 전에 응용 프로그램 수준에서 무엇을 할 수 있는지 먼저 살펴봐야 한다.

데드락을 피하기 위해 풀 크기를 계산하는 것은 다음과 같은 공식으로 가능하다.

poolsize=Tn(Cm1)+1poolsize = T_{n} * (C_{m}- 1) + 1

어떤 작업을 수행하기 위해 4개의 커넥션이 필요한 세 개의 스레드 Tn=3T_{n}= 3 이 있다고 가정하자. 데드락이 절대 발생하지 않도록 보장하기 위해 필요한 풀 크기는 3(41)+1=103 * (4 - 1) + 1 = 10 이다.

이는 반드시 최적의 풀 크기는 아닐 수 있지만, 데드락을 피하기 위한 최소한의 크기이다.
어떤 환경에서는 JTA (JavaTransaction Manager) 를 사용하여 현재 트랜잭션에서 이미 커넥션을 보유하고 있는 스레드에 getConnection() 에서 동일한 Connection 을 반환함으로써 필요한 커넥션을 크게 줄일 수 있다.

액터란?

동시성 프로그래밍에서 독립적으로 실행되는 엔티티를 의미한다. 액터들은 서로 메시지를 주고받으며 통신하고 동작한다. 각 액터는 자신만의 상태를 가지고, 외부에서 직접 접근할 수 없다. 대신, 액터는 받은 메시지에 따라 내부 상태를 변경하거나 다른 액터에 메시지를 보내는 등의 작업을 수행한다.

그러나 위의 맥락에서는 일반적인 액터 모델의 의미가 아닌 동시성을 다루는 작업 단위나 엔티티를 의미한다.

  • [?] 액터가 여러 커넥션을 얻는 경우 예시
  • 액터가 동시에 여러 테이블이나 데이터 셋에서 정보를 가져와야 할 때, 각 쿼리에 대해 별도 커넥션을 확보할 수 있다.
  • 여러 외부 서비스와 통신해야 하는 작업을 수행할 때, 각 서비스마다 별도의 커넥션을 열 수 있다.
  • 액터가 여러 리소스에 대한 동시 트랜잭션을 관리해야 할 때, 각 리소스에 대한 커넥션을 유지하게 된다.